# -*- coding: utf-8 -*-

"""
@desc: 基于用户的协同过滤算法UserCF
@author: 然诺
@contact: rannuo1010@gmail.com
@time: 2019/4/10 23:40
@file: UserCF.py
"""

from numpy import *


def user_similarity(train):
    """
    计算用户相似度矩阵和相关用户字典
    :param train: 训练集集
    :return: 用户相似度矩阵W,W[u][v]表示u,v的相似度
    :return: 相关用户user_related_users字典
    """
    # 1.建立物品-用户的倒排表
    item_users = dict()
    for u, items in train.items():
        for i in items.keys():
            if i not in item_users:
                item_users[i] = set()
            item_users[i].add(u)

    # 2.找到和目标用户相似的用户集合
    N = dict()
    C = dict()
    for _, users in item_users.items():
        for u in users:
            if u not in N.keys():
                N.setdefault(u, 0)
            N[u] += 1
            for v in users:
                if u == v:
                    continue
                C.setdefault(u, {})
                C[u].setdefault(v, 0)
                C[u][v] += 1
                # C[u][v] += 1 / math.log(1 + len(users)) # User-IIF算法

    # 3.计算相似度矩阵W[u][v]
    W = dict()
    for u, related_users in C.items():
        W.setdefault(u, dict())
        for v, cuv in related_users.items():
            W[u][v] = cuv / math.sqrt(N[u] * N[v] * 1.0)
    return W


def recommend(user, train, N, K, W):
    """
    通过相似度矩阵W得到和user相似的rank字典
    :param user: 目标用户
    :param train: 训练集集
    :param K: 每个用户选出K个和他兴趣最相似的用户
    :param W: 相似度矩阵
    :return: rank字典，包含了所有兴趣程度不为0的物品，按照从大到小排序
    """
    rank = dict()
    interacted_items = train.get(user, {})
    for v, wuv in sorted(W[user].items(), key=lambda x: x[1], reverse=True)[0:K]:
        for i, rvi in train[v].items():
            if i in interacted_items:
                continue
            rank.setdefault(i, 0)
            # rank[i] += wuv * rvi
            rank[i] += wuv
    return dict(sorted(rank.items(), key=lambda x: x[1], reverse=True)[0:N])


def calc_recall(train, test, N, K, W):
    """
    计算召回率
    召回率描述有多少比例的用户—物品评分记录包含在最终的推荐列表中
    通过【准确率/召回率】评测推荐算法的精度
    :param train:训练集
    :param test:测试集
    :param N:TopN推荐中N数目
    :param K:相邻用户个数
    :return:召回率
    """
    hit = 0  # 预测准确的数目
    total = 0  # 所有行为总数
    for user in train.keys():
        # 用户u在测试集上喜欢的物品集合为T(u)
        tu = test.get(user, {})
        # 对用户u推荐N个物品（记为R(u)）
        ru = recommend(user, train, N, K, W)
        for item, _ in ru.items():
            if item in tu:
                hit += 1
        total += len(tu)
    return hit / (total * 1.0)


def calc_precision(train, test, N, K, W):
    """
    计算精确率
    准确率描述最终的推荐列表中有多少比例是发生过的用户—物品评分记录
    :param train:训练集
    :param test:测试集
    :param N:TopN推荐中N数目
    :param K:相邻用户个数
    :return:准确率
    """
    hit = 0  # 预测准确的数目
    total = 0  # 所有行为总数
    for user in train.keys():
        # 用户u在测试集上喜欢的物品集合为T(u)
        tu = test.get(user, {})
        # 对用户u推荐N个物品（记为R(u)）
        rank = recommend(user, train, N, K, W)
        for item, _ in rank.items():
            if item in tu:
                hit += 1
        total += N
    return hit / (total * 1.0)


def calc_coverage(train, N, K, W):
    """
    计算覆盖率
    覆盖率反映了推荐算法发掘长尾的能力，覆盖率越高，说明推荐算法越能够将长尾中的物品推荐给用户
    :param train:训练集 字典user->items
    :param N: topN推荐中N
    :param K:
    :param W:
    """
    recommend_items = set()
    all_items = set()
    for user in train.keys():
        for item in train[user].keys():
            all_items.add(item)
        rank = recommend(user, train, N, K, W)
        for item, _ in rank.items():
            recommend_items.add(item)
    return len(recommend_items) / (len(all_items) * 1.0)


def calc_popularity(train, N, K, W):
    """
    计算平均流行度 信息的熵
    如果推荐出的物品都很热门，说明推荐的新颖度较低，否则说明推荐结果比较新颖
    :param train:训练集 字典user->items
    :param N:topN推荐中N
    :param k:相邻个数
    :return:平均流行度
    """
    item_popularity = dict()
    for user, items in train.items():
        for item in items.keys():
            if item not in item_popularity:
                item_popularity.setdefault(item, 0)
            item_popularity[item] += 1
    ret = 0
    n = 0
    for user in train.keys():
        rank = recommend(user, train, N, K, W)
        for item, _ in rank.items():
            # 流行度分布满足长尾分布，在取对数后，流行度的平均值更加稳定
            ret += math.log(1 + item_popularity[item])
            n += 1
    return ret / (n * 1.0)


def calc_accuracy(train, test, N, K, W):
    hit = 0
    recall_total = 0
    precision_total = 0
    for user in train.keys():
        # 用户u在测试集上喜欢的物品集合为T(u)
        tu = test.get(user, {})
        # 对用户u推荐N个物品（记为R(u)）
        rank = recommend(user, train, N, K, W)
        for item, _ in rank.items():
            if item in tu:
                hit += 1
        recall_total += len(tu)
        precision_total += N
    recall = hit / recall_total * 1.0
    precision = hit / precision_total * 1.0
    # return 2 * recall * precision / (recall + precision)
    return precision / recall


def calc_evaluation_index(train, test, N, K, W):
    """
    计算评测指标
    :param train: 训练集
    :param test: 测试集
    :param N: 推荐列表长度(Top-N)
    :param K: 邻接数
    :param W: 相似矩阵
    :return: 召回率、准确率、覆盖率、平均流行度、算法精度
    """
    hit = 0
    tu_total = 0
    ru_total = 0
    popularity_ret = 0
    recommend_items = set()
    all_items = set()
    item_popularity = dict()

    for user, items in train.items():
        for item in items.keys():
            if item not in item_popularity:
                item_popularity.setdefault(item, 0)
            item_popularity[item] += 1

    for user in train.keys():
        for item in train[user].keys():
            all_items.add(item)
        # 用户u在测试集上喜欢的物品集合为T(u)
        tu = test.get(user, {})
        # 对用户u推荐N个物品（记为R(u)）
        ru = recommend(user, train, N, K, W)
        for item, _ in ru.items():
            if item in tu:
                hit += 1
            popularity_ret += math.log(1 + item_popularity[item])  # 流行度分布满足长尾分布，在取对数后，流行度的平均值更加稳定
            recommend_items.add(item)
        tu_total += len(tu)
        ru_total += N

    recall = hit / tu_total * 1.0  # 计算召回率
    precision = hit / ru_total * 1.0  # 计算准确率
    coverage = len(recommend_items) / len(all_items) * 1.0  # 计算覆盖率
    popularity = popularity_ret / ru_total  # 计算流行度
    accuracy = precision / recall  # 计算精确度
    return recall, precision, coverage, popularity, accuracy
